package tld.test.interpreter
{
	public class Evaluator
	{
		private static const _chars:Object =
		{
			"n" : "\n",
			"r" : "\r",
			"t" : "\t",
			"v" : "\v"
		}
		public function Evaluator() { super(); }
		
		public function eval(parser:Parser, 
			expression:Expression, context:IContext):Object
		{
			// TODO: fix control flow
			var callable:Object;
			var args:Array;
			var subExpression:Expression;
			var first:String;
			var combinedContext:Object = context.getNames();
			var maybeNumber:Number;
			trace("evaluating", expression);
			if (expression)
			{
				if (expression.head == "if" ||
					expression.head == "&&" ||
					expression.head == "||")
				{
					// special forms (until we have macros)
					return this.lazyEval(parser, expression, context);
				}
				if (expression.head in combinedContext)
				{
					callable = combinedContext[expression.head];
				}
				else if (expression.head)
				{
					first = expression.head.charAt();
					if (first == "'")
					{
						first = expression.head.charAt(1);
						if (first == "(")
						{
							// this is an array (list)
							subExpression = 
								parser.readExpression(expression.head.substr(1));
							trace("future array:", subExpression);
							if (!subExpression.tail) subExpression.tail = [];
							subExpression.tail.unshift(new Expression(subExpression.head));
							// TODO: we could use one array here instead of
							// two, optimize later
							args = [];
							for each (var argExp:Expression in subExpression.tail)
								args.push(this.eval(parser, argExp, context));
							return args;
						}
						else
						{
							// this is an expression with head only
							return parser.readExpression(expression.head.substr(1)); 
						}
					}
					else if (first == "(")
					{
						// lambda funcall form, not implemented really...
						subExpression = parser.readExpression(expression.head);
						callable = this.eval(parser, subExpression, context);
					}
					else if (first == "\"")
					{
						// a string
						return this.readString(expression.head);
					}
					else
					{
						// a symbol
						maybeNumber = parseFloat(expression.head);
						if (!isNaN(maybeNumber)) return maybeNumber;
						else return combinedContext[expression.head];
					}
				}
				trace("arguments to eval: ", expression.tail);
				for each (var arg:Expression in expression.tail)
				{
					if (!args) args = [];
					args.push(this.eval(parser, arg, context));
				}
				if (callable is Function)
				{
					return (callable as Function).apply(context, args);
				}
				else if (callable is Expression)
				{
					callable = this.eval(parser, callable as Expression, context);
					return (callable as Function).apply(context, args);
				}
				else if (callable is Quote)
				{
					return parser.read((callable as Quote).text);
				}
				// this looks redundant
				else return callable;
			}
			else return null;
			// probably shouldn't get here
			return null;
		}
		
		private function lazyEval(parser:Parser, 
			expression:Expression, context:IContext):Object
		{
			var result:Object;
			var i:int;
			
			switch (expression.head)
			{
				case "if":
					if (this.eval(parser, expression.tail[0], context))
						result = this.eval(parser, expression.tail[1], context);
					else result = this.eval(parser, expression.tail[2], context);
					break;
				case "&&":
					if (expression.tail)
					{
						do
						{
							result = this.eval(parser, expression.tail[i], context);
							i++;
						}
						while (result && i < expression.tail.length);
					}
					else result = true;
					break;
				case "||":
					if (expression.tail)
					{
						do
						{
							result = this.eval(parser, expression.tail[i], context);
							i++;
						}
						while (!result && i < expression.tail.length);
					}
					else result = false;
					break;
			}
			return result;
		}
		
		private function readString(input:String):String
		{
			var escaped:Boolean;
			var i:int;
			var len:int = input.length - 1;
			var result:String = "";
			var current:String;
			var code:String;
			var codeChar:Number;
			
			while (i++ < len)
			{
				current = input.charAt(i);
				if (current == "\\")
				{
					if (escaped) result  += current;
					escaped = !escaped;
				}
				else if (escaped)
				{
					// TODO: support unicodes in strings
					if (current == "x")
					{
						// need to check out of bounds
						code = input.substr(i + 1, 2);
						i += 2;
						codeChar = parseInt(code, 16);
						if (!isNaN(codeChar))
							result += String.fromCharCode(codeChar);
						else throw "Incorrect character sequence";
					}
					else if (current in _chars)
						result += _chars[current];
					else throw "Unrecognized character sequence";
					escaped = false;
				}
				else result += current;
			}
			return result.substr(0, result.length - 1);
		}
	}
}